/*
 ------------------------------------------------------------------------------
 Copyright (C) 2013 Eternal Games.

 This file is part of the EternalTech source code.

 The EternalTech source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The EternalTech source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the EternalTech source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


//
// cl_cinematic.cpp - cinematic playback
//


#include "client.h"


struct cinematic_t {
	bool					playing;

	char					name[MAX_PATH_LENGTH];
	int						flags;

	fileHandle_t			file;
	int						size;
	int						offset;

	int						startTime;

	int						frameRate;
	int						frameWidth;
	int						frameHeight;
	int						frameCount;
	byte *					frameBuffer[2];

	roqChunk_t				chunk;

	roqQuadVector_t			quadVectors[256];
	roqQuadCell_t			quadCells[256];
};

static short				cin_v2rTable[256];
static short				cin_u2gTable[256];
static short				cin_v2gTable[256];
static short				cin_u2bTable[256];

static short				cin_sqrTable[256];

static short				cin_soundSamples[ROQ_CHUNK_MAX_SIZE >> 1];

static cinematic_t			cin_cinematics[MAX_CINEMATICS];


/*
 ==================
 CIN_QuadVector4x4
 ==================
*/
static void CIN_QuadVector4x4 (cinematic_t *cin, int x, int y, const byte *indices){

	dword	*src, *dst;
	int		xOfs, yOfs;
	int		i;

	for (i = 0; i < 4; i++){
		xOfs = x + 2 * (i & 1);
		yOfs = y + 2 * (i >> 1);

		src = (dword *)cin->quadVectors + (indices[i] << 2);
		dst = (dword *)cin->frameBuffer[0] + (yOfs * cin->frameWidth + xOfs);

		dst[0] = src[0];
		dst[1] = src[1];

		dst += cin->frameWidth;

		dst[0] = src[2];
		dst[1] = src[3];
	}
}

/*
 ==================
 CIN_QuadVector8x8
 ==================
*/
static void CIN_QuadVector8x8 (cinematic_t *cin, int x, int y, const byte *indices){

	dword	*src, *dst;
	int		xOfs, yOfs;
	int		i;

	for (i = 0; i < 4; i++){
		xOfs = x + 4 * (i & 1);
		yOfs = y + 4 * (i >> 1);

		src = (dword *)cin->quadVectors + (indices[i] << 2);
		dst = (dword *)cin->frameBuffer[0] + (yOfs * cin->frameWidth + xOfs);

		dst[0] = src[0];
		dst[1] = src[0];
		dst[2] = src[1];
		dst[3] = src[1];

		dst += cin->frameWidth;

		dst[0] = src[0];
		dst[1] = src[0];
		dst[2] = src[1];
		dst[3] = src[1];

		dst += cin->frameWidth;

		dst[0] = src[2];
		dst[1] = src[2];
		dst[2] = src[3];
		dst[3] = src[3];

		dst += cin->frameWidth;

		dst[0] = src[2];
		dst[1] = src[2];
		dst[2] = src[3];
		dst[3] = src[3];
	}
}

/*
 ==================
 CIN_MotionBlock4x4
 ==================
*/
static void CIN_MotionBlock4x4 (cinematic_t *cin, int x, int y, int xMean, int yMean, int xyMotion){

	dword	*src, *dst;
	int		xMot, yMot;

	xMot = x + xMean - (xyMotion >> 4);
	yMot = y + yMean - (xyMotion & 15);

	src = (dword *)cin->frameBuffer[1] + (yMot * cin->frameWidth + xMot);
	dst = (dword *)cin->frameBuffer[0] + (y * cin->frameWidth + x);

	dst[0] = src[0];
	dst[1] = src[1];
	dst[2] = src[2];
	dst[3] = src[3];

	src += cin->frameWidth;
	dst += cin->frameWidth;

	dst[0] = src[0];
	dst[1] = src[1];
	dst[2] = src[2];
	dst[3] = src[3];

	src += cin->frameWidth;
	dst += cin->frameWidth;

	dst[0] = src[0];
	dst[1] = src[1];
	dst[2] = src[2];
	dst[3] = src[3];

	src += cin->frameWidth;
	dst += cin->frameWidth;

	dst[0] = src[0];
	dst[1] = src[1];
	dst[2] = src[2];
	dst[3] = src[3];
}

/*
 ==================
 CIN_MotionBlock8x8
 ==================
*/
static void CIN_MotionBlock8x8 (cinematic_t *cin, int x, int y, int xMean, int yMean, int xyMotion){

	dword	*src, *dst;
	int		xMot, yMot;

	xMot = x + xMean - (xyMotion >> 4);
	yMot = y + yMean - (xyMotion & 15);

	src = (dword *)cin->frameBuffer[1] + (yMot * cin->frameWidth + xMot);
	dst = (dword *)cin->frameBuffer[0] + (y * cin->frameWidth + x);

	dst[0] = src[0];
	dst[1] = src[1];
	dst[2] = src[2];
	dst[3] = src[3];
	dst[4] = src[4];
	dst[5] = src[5];
	dst[6] = src[6];
	dst[7] = src[7];

	src += cin->frameWidth;
	dst += cin->frameWidth;

	dst[0] = src[0];
	dst[1] = src[1];
	dst[2] = src[2];
	dst[3] = src[3];
	dst[4] = src[4];
	dst[5] = src[5];
	dst[6] = src[6];
	dst[7] = src[7];

	src += cin->frameWidth;
	dst += cin->frameWidth;

	dst[0] = src[0];
	dst[1] = src[1];
	dst[2] = src[2];
	dst[3] = src[3];
	dst[4] = src[4];
	dst[5] = src[5];
	dst[6] = src[6];
	dst[7] = src[7];

	src += cin->frameWidth;
	dst += cin->frameWidth;

	dst[0] = src[0];
	dst[1] = src[1];
	dst[2] = src[2];
	dst[3] = src[3];
	dst[4] = src[4];
	dst[5] = src[5];
	dst[6] = src[6];
	dst[7] = src[7];

	src += cin->frameWidth;
	dst += cin->frameWidth;

	dst[0] = src[0];
	dst[1] = src[1];
	dst[2] = src[2];
	dst[3] = src[3];
	dst[4] = src[4];
	dst[5] = src[5];
	dst[6] = src[6];
	dst[7] = src[7];

	src += cin->frameWidth;
	dst += cin->frameWidth;

	dst[0] = src[0];
	dst[1] = src[1];
	dst[2] = src[2];
	dst[3] = src[3];
	dst[4] = src[4];
	dst[5] = src[5];
	dst[6] = src[6];
	dst[7] = src[7];

	src += cin->frameWidth;
	dst += cin->frameWidth;

	dst[0] = src[0];
	dst[1] = src[1];
	dst[2] = src[2];
	dst[3] = src[3];
	dst[4] = src[4];
	dst[5] = src[5];
	dst[6] = src[6];
	dst[7] = src[7];

	src += cin->frameWidth;
	dst += cin->frameWidth;

	dst[0] = src[0];
	dst[1] = src[1];
	dst[2] = src[2];
	dst[3] = src[3];
	dst[4] = src[4];
	dst[5] = src[5];
	dst[6] = src[6];
	dst[7] = src[7];
}

/*
 ==================
 CIN_DecodeInfo
 ==================
*/
static void CIN_DecodeInfo (cinematic_t *cin, const byte *data){

	if (cin->frameBuffer[0] && cin->frameBuffer[1])
		return;		// Already allocated

	// Allocate the frame buffers
	cin->frameWidth = data[0] | (data[1] << 8);
	cin->frameHeight = data[2] | (data[3] << 8);

	if ((cin->frameWidth & 15) || (cin->frameHeight & 15))
		Com_Error(false, "CIN_DecodeInfo: video dimensions not divisible by 16");

	cin->frameBuffer[0] = (byte *)Mem_Alloc16(cin->frameWidth * cin->frameHeight * 4, TAG_COMMON);
	cin->frameBuffer[1] = (byte *)Mem_Alloc16(cin->frameWidth * cin->frameHeight * 4, TAG_COMMON);
}

/*
 ==================
 CIN_DecodeCodebook
 ==================
*/
static void CIN_DecodeCodebook (cinematic_t *cin, const byte *data){

	int		numQuadVectors;
	int		numQuadCells;
	int		i;

	if (!cin->chunk.flags){
		numQuadVectors = 256;
		numQuadCells = 256;
	}
	else {
		numQuadVectors = (cin->chunk.flags >> 8) & 255;
		numQuadCells = (cin->chunk.flags >> 0) & 255;

		if (!numQuadVectors)
			numQuadVectors = 256;
	}

#if defined SIMD_X86

	__m64	mmRGBA;
	__m64	mmPixels[4];

	// Decode YUV quad vectors to RGB
	for (i = 0; i < numQuadVectors; i++){
		mmRGBA = _mm_set_pi16(255, cin_u2bTable[data[4]], cin_u2gTable[data[4]] + cin_v2gTable[data[5]], cin_v2rTable[data[5]]);

		mmPixels[0] = _mm_add_pi16(mmRGBA, _mm_set1_pi16(data[0]));
		mmPixels[1] = _mm_add_pi16(mmRGBA, _mm_set1_pi16(data[1]));
		mmPixels[2] = _mm_add_pi16(mmRGBA, _mm_set1_pi16(data[2]));
		mmPixels[3] = _mm_add_pi16(mmRGBA, _mm_set1_pi16(data[3]));

		((__m64 *)(&cin->quadVectors[i]))[0] = _mm_packs_pu16(mmPixels[0], mmPixels[1]);
		((__m64 *)(&cin->quadVectors[i]))[1] = _mm_packs_pu16(mmPixels[2], mmPixels[3]);

		data += 6;
	}

	_mm_empty();

#else

	int		r, g, b;

	// Decode YUV quad vectors to RGB
	for (i = 0; i < numQuadVectors; i++){
		r = cin_v2rTable[data[5]];
		g = cin_u2gTable[data[4]] + cin_v2gTable[data[5]];
		b = cin_u2bTable[data[4]];

		cin->quadVectors[i].pixel[0][0] = odMath::ClampByte(r + data[0]);
		cin->quadVectors[i].pixel[0][1] = odMath::ClampByte(g + data[0]);
		cin->quadVectors[i].pixel[0][2] = odMath::ClampByte(b + data[0]);
		cin->quadVectors[i].pixel[0][3] = 255;

		cin->quadVectors[i].pixel[1][0] = odMath::ClampByte(r + data[1]);
		cin->quadVectors[i].pixel[1][1] = odMath::ClampByte(g + data[1]);
		cin->quadVectors[i].pixel[1][2] = odMath::ClampByte(b + data[1]);
		cin->quadVectors[i].pixel[1][3] = 255;

		cin->quadVectors[i].pixel[2][0] = odMath::ClampByte(r + data[2]);
		cin->quadVectors[i].pixel[2][1] = odMath::ClampByte(g + data[2]);
		cin->quadVectors[i].pixel[2][2] = odMath::ClampByte(b + data[2]);
		cin->quadVectors[i].pixel[2][3] = 255;

		cin->quadVectors[i].pixel[3][0] = odMath::ClampByte(r + data[3]);
		cin->quadVectors[i].pixel[3][1] = odMath::ClampByte(g + data[3]);
		cin->quadVectors[i].pixel[3][2] = odMath::ClampByte(b + data[3]);
		cin->quadVectors[i].pixel[3][3] = 255;

		data += 6;
	}

#endif

	// Copy quad cells
	for (i = 0; i < numQuadCells; i++)
		((dword *)cin->quadCells)[i] = ((const dword *)data)[i];
}

/*
 ==================
 CIN_DecodeVideo
 ==================
*/
static void CIN_DecodeVideo (cinematic_t *cin, const byte *data){

	int		vqPos, vqData, vqCode;
	int		xPos, yPos, xOfs, yOfs;
	int		xMean, yMean;
	int		x = 0, y = 0;
	int		i;

	if (!cin->frameBuffer[0] || !cin->frameBuffer[1])
		return;		// No frame buffers

	vqPos = 0;
	vqData = 0;

	xMean = 8 - (char)((cin->chunk.flags >> 8) & 255);
	yMean = 8 - (char)((cin->chunk.flags >> 0) & 255);

	while (1){
		for (yPos = y; yPos < y + 16; yPos += 8){
			for (xPos = x; xPos < x + 16; xPos += 8){
				if (vqPos)
					vqPos--;
				else {
					vqPos = 7;
					vqData = data[0] | (data[1] << 8);

					data += 2;
				}

				vqCode = vqData & 0xC000;
				vqData <<= 2;

				switch (vqCode){
				case ROQ_VQ_MOT:

					break;
				case ROQ_VQ_FCC:
					CIN_MotionBlock8x8(cin, xPos, yPos, xMean, yMean, *data);

					data += 1;

					break;
				case ROQ_VQ_SLD:
					CIN_QuadVector8x8(cin, xPos, yPos, cin->quadCells[*data].index);

					data += 1;

					break;
				case ROQ_VQ_CCC:
					for (i = 0; i < 4; i++){
						xOfs = xPos + 4 * (i & 1);
						yOfs = yPos + 4 * (i >> 1);

						if (vqPos)
							vqPos--;
						else {
							vqPos = 7;
							vqData = data[0] | (data[1] << 8);

							data += 2;
						}

						vqCode = vqData & 0xC000;
						vqData <<= 2;

						switch (vqCode){
						case ROQ_VQ_MOT:

							break;
						case ROQ_VQ_FCC:
							CIN_MotionBlock4x4(cin, xOfs, yOfs, xMean, yMean, *data);

							data += 1;

							break;
						case ROQ_VQ_SLD:
							CIN_QuadVector4x4(cin, xOfs, yOfs, cin->quadCells[*data].index);

							data += 1;

							break;
						case ROQ_VQ_CCC:
							CIN_QuadVector4x4(cin, xOfs, yOfs, data);

							data += 4;

							break;
						default:
							Com_Error(false, "CIN_DecodeVideo: bad VQ code (%i)", vqCode);
						}
					}

					break;
				default:
					Com_Error(false, "CIN_DecodeVideo: bad VQ code (%i)", vqCode);
				}
			}
		}

		x += 16;
		if (x == cin->frameWidth){
			x = 0;

			y += 16;
			if (y == cin->frameHeight)
				break;
		}
	}

	cin->frameCount++;

	// Swap the frame buffers
	if (cin->frameCount == 1){
		Mem_Copy(cin->frameBuffer[1], cin->frameBuffer[0], cin->frameWidth * cin->frameHeight * 4);
		return;
	}

	Swap(cin->frameBuffer[0], cin->frameBuffer[1]);
}

/*
 ==================
 CIN_DecodeSoundMono
 ==================
*/
static void CIN_DecodeSoundMono (cinematic_t *cin, const byte *data){

	int		prev;
	int		i;

	if (cin->flags & CIN_SILENT)
		return;

	prev = cin->chunk.flags;

	for (i = 0; i < cin->chunk.size; i++){
		prev = (short)(prev + cin_sqrTable[data[i]]);

		cin_soundSamples[i] = (short)prev;
	}

	// Submit the sound samples
	S_RawSamples(cin_soundSamples, cin->chunk.size, 22050, false, 1.0f);
}

/*
 ==================
 CIN_DecodeSoundStereo
 ==================
*/
static void CIN_DecodeSoundStereo (cinematic_t *cin, const byte *data){

	int		prevL, prevR;
	int		i;

	if (cin->flags & CIN_SILENT)
		return;

	prevL = (cin->chunk.flags & 0xFF00) << 0;
	prevR = (cin->chunk.flags & 0x00FF) << 8;

	for (i = 0; i < cin->chunk.size; i += 2){
		prevL = (short)(prevL + cin_sqrTable[data[i+0]]);
		prevR = (short)(prevR + cin_sqrTable[data[i+1]]);

		cin_soundSamples[i+0] = (short)prevL;
		cin_soundSamples[i+1] = (short)prevR;
	}

	// Submit the sound samples
	S_RawSamples(cin_soundSamples, cin->chunk.size >> 1, 22050, true, 1.0f);
}

/*
 ==================
 CIN_DecodeChunk
 ==================
*/
static bool CIN_DecodeChunk (cinematic_t *cin){

	byte	buffer[ROQ_CHUNK_HEADER_SIZE + ROQ_CHUNK_MAX_SIZE];
	byte	*data;

	if (cin->offset >= cin->size)
		return false;	// Finished

	data = buffer;

	// Read and decode the first chunk header if needed
	if (cin->offset == ROQ_CHUNK_HEADER_SIZE){
		cin->offset += FS_Read(cin->file, buffer, ROQ_CHUNK_HEADER_SIZE);

		cin->chunk.id = data[0] | (data[1] << 8);
		cin->chunk.size = data[2] | (data[3] << 8) | (data[4] << 16) | (data[5] << 24);
		cin->chunk.flags = data[6] | (data[7] << 8);
	}

	// Read the chunk data and the next chunk header
	if (cin->chunk.size > ROQ_CHUNK_MAX_SIZE)
		Com_Error(false, "CIN_DecodeChunk: bad chunk size (%u)", cin->chunk.size);

	if (cin->offset + cin->chunk.size >= cin->size)
		cin->offset += FS_Read(cin->file, buffer, cin->chunk.size);
	else
		cin->offset += FS_Read(cin->file, buffer, cin->chunk.size + ROQ_CHUNK_HEADER_SIZE);

	// Decode the chunk data
	switch (cin->chunk.id){
	case ROQ_QUAD_INFO:
		CIN_DecodeInfo(cin, data);
		break;
	case ROQ_QUAD_CODEBOOK:
		CIN_DecodeCodebook(cin, data);
		break;
	case ROQ_QUAD_VQ:
		CIN_DecodeVideo(cin, data);
		break;
	case ROQ_SOUND_MONO:
		CIN_DecodeSoundMono(cin, data);
		break;
	case ROQ_SOUND_STEREO:
		CIN_DecodeSoundStereo(cin, data);
		break;
	default:
		Com_Error(false, "CIN_DecodeChunk: bad chunk id (%u)", cin->chunk.id);
	}

	data += cin->chunk.size;

	// Decode the next chunk header if needed
	if (cin->offset >= cin->size)
		return true;

	cin->chunk.id = data[0] | (data[1] << 8);
	cin->chunk.size = data[2] | (data[3] << 8) | (data[4] << 16) | (data[5] << 24);
	cin->chunk.flags = data[6] | (data[7] << 8);

	return true;
}


// ============================================================================


/*
 ==================
 CIN_HandleForCinematic

 Finds a free cinHandle_t
 ==================
*/
static cinematic_t *CIN_HandleForCinematic (cinHandle_t *handle){

	cinematic_t	*cin;
	int			i;

	for (i = 0, cin = cin_cinematics; i < MAX_CINEMATICS; i++, cin++){
		if (!cin->playing)
			break;
	}

	if (i == MAX_CINEMATICS)
		Com_Error(false, "CIN_HandleForCinematic: none free");

	*handle = i + 1;

	return cin;
}

/*
 ==================
 CIN_GetCinematicByHandle

 Returns a cinematic_t for the given cinHandle_t
 ==================
*/
static cinematic_t *CIN_GetCinematicByHandle (cinHandle_t handle){

	cinematic_t	*cin;

	if (handle <= 0 || handle > MAX_CINEMATICS)
		Com_Error(false, "CIN_GetCinematicByHandle: handle out of range");

	cin = &cin_cinematics[handle - 1];

	if (!cin->playing)
		Com_Error(false, "CIN_GetCinematicByHandle: invalid handle");

	return cin;
}

/*
 ==================
 CIN_PlayCinematic
 ==================
*/
cinHandle_t CIN_PlayCinematic (const char *name, int flags){

	cinematic_t		*cin;
	cinHandle_t		handle;
	fileHandle_t	file;
	byte			buffer[ROQ_CHUNK_HEADER_SIZE];
	word			id, fps;
	int				size;
	int				i;

	// See if already playing
	for (i = 0, cin = cin_cinematics; i < MAX_CINEMATICS; i++, cin++){
		if (!cin->playing)
			continue;

		if (!Str_ICompare(cin->name, name)){
			if (cin->flags != flags)
				continue;

			return i + 1;
		}
	}

	// Open the file
	size = FS_OpenFile(name, FS_READ, &file);
	if (!file){
		if (flags & CIN_SYSTEM){
			cls.playingCinematic = false;

			Com_Printf("Cinematic %s not found\n", name);
		}

		return 0;
	}

	// Read the file header
	FS_Read(file, buffer, ROQ_CHUNK_HEADER_SIZE);

	id = buffer[0] | (buffer[1] << 8);
	fps = buffer[6] | (buffer[7] << 8);

	if (id != ROQ_ID){
		FS_CloseFile(file);

		if (flags & CIN_SYSTEM){
			cls.playingCinematic = false;

			Com_Printf("Cinematic %s is not a RoQ file\n", name);
		}

		return 0;
	}

	// Play the cinematic
	cin = CIN_HandleForCinematic(&handle);

	if (flags & CIN_SYSTEM){
		cls.playingCinematic = true;

		Com_Printf("Playing cinematic %s\n", name);
	}

	// Fill it in
	cin->playing = true;
	Str_Copy(cin->name, name, sizeof(cin->name));
	cin->flags = flags;
	cin->file = file;
	cin->size = size;
	cin->offset = ROQ_CHUNK_HEADER_SIZE;
	cin->startTime = 0;
	cin->frameRate = (fps) ? fps : 30;
	cin->frameWidth = 0;
	cin->frameHeight = 0;
	cin->frameCount = 0;
	cin->frameBuffer[0] = NULL;
	cin->frameBuffer[1] = NULL;

	return handle;
}

/*
 ==================
 CIN_UpdateCinematic
 ==================
*/
cinData_t CIN_UpdateCinematic (cinHandle_t handle, int time){

	cinematic_t	*cin;
	cinData_t	data;
	int			frame;

	cin = CIN_GetCinematicByHandle(handle);

	// If we don't have a frame yet, set the start time
	if (!cin->frameCount)
		cin->startTime = time;

	// Check if a new frame is needed
	frame = (time - cin->startTime) * cin->frameRate / 1000;
	if (frame < 1)
		frame = 1;

	if (frame <= cin->frameCount){
		data.image = cin->frameBuffer[1];
		data.dirty = false;

		data.width = cin->frameWidth;
		data.height = cin->frameHeight;

		return data;
	}

	// If we're dropping frames
	if (frame > cin->frameCount + 1){
		if (cin->flags & CIN_SYSTEM)
			Com_Printf(S_COLOR_YELLOW "Dropped cinematic frame: %i > %i (%s)\n", frame, cin->frameCount + 1, cin->name);
		else
			Com_DPrintf(S_COLOR_YELLOW "Dropped cinematic frame: %i > %i (%s)\n", frame, cin->frameCount + 1, cin->name);

		frame = cin->frameCount + 1;

		// Reset the start time
		cin->startTime = time - frame * 1000 / cin->frameRate;
	}

	// Get the desired frame
	while (frame > cin->frameCount){
		// Decode a chunk
		if (CIN_DecodeChunk(cin))
			continue;

		// If we get here, the cinematic has finished
		if (!cin->frameCount || !(cin->flags & CIN_LOOPING)){
			data.image = NULL;
			data.dirty = false;

			data.width = 0;
			data.height = 0;

			return data;
		}

		// Reset the cinematic
		FS_Seek(cin->file, ROQ_CHUNK_HEADER_SIZE, FS_SEEK_SET);

		cin->offset = ROQ_CHUNK_HEADER_SIZE;
		cin->startTime = time;
		cin->frameCount = 0;

		// Get the first frame
		frame = 1;
	}

	data.image = cin->frameBuffer[1];
	data.dirty = true;

	data.width = cin->frameWidth;
	data.height = cin->frameHeight;

	return data;
}

/*
 ==================
 CIN_ResetCinematic
 ==================
*/
void CIN_ResetCinematic (cinHandle_t handle, int time){

	cinematic_t	*cin;

	cin = CIN_GetCinematicByHandle(handle);

	// Reset the cinematic
	FS_Seek(cin->file, ROQ_CHUNK_HEADER_SIZE, FS_SEEK_SET);

	cin->offset = ROQ_CHUNK_HEADER_SIZE;
	cin->startTime = time;
	cin->frameCount = 0;
}

/*
 ==================
 CIN_StopCinematic
 ==================
*/
void CIN_StopCinematic (cinHandle_t handle){

	cinematic_t	*cin;

	cin = CIN_GetCinematicByHandle(handle);

	// Stop the cinematic
	if (cin->flags & CIN_SYSTEM){
		cls.playingCinematic = false;

		// Make sure sounds aren't playing
		S_StopAllSounds();
	}

	// Free the frame buffers
	if (cin->frameBuffer[0])
		Mem_Free(cin->frameBuffer[0]);
	if (cin->frameBuffer[1])
		Mem_Free(cin->frameBuffer[1]);

	// Close the file
	if (cin->file)
		FS_CloseFile(cin->file);

	Mem_Fill(cin, 0, sizeof(cinematic_t));
}


/*
 ==============================================================================

 CONSOLE COMMANDS

 ==============================================================================
*/


/*
 ==================
 CIN_PlayCinematic_f
 ==================
*/
static void CIN_PlayCinematic_f (){

	char	name[MAX_PATH_LENGTH];

	if (Cmd_Argc() != 2){
		Com_Printf("Usage: playCinematic <name>\n");
		return;
	}

	Str_Copy(name, Cmd_Argv(1), sizeof(name));
	Str_DefaultFilePath(name, sizeof(name), "videos");
	Str_DefaultFileExtension(name, sizeof(name), ".RoQ");

	// If running a local server, kill it
	SV_Shutdown("Server quit");

	// If connected to a server, disconnect
	CL_Disconnect(true);

	// Play the cinematic
	cls.cinematicHandle = CIN_PlayCinematic(name, CIN_SYSTEM);
}

/*
 ==================
 CIN_ListCinematics_f
 ==================
*/
static void CIN_ListCinematics_f (){

	cinematic_t	*cin;
	int			count = 0, bytes = 0;
	int			i;

	Com_Printf("\n");
	Com_Printf("      -w-- -h-- -size- fps -name-----------\n");

	for (i = 0, cin = cin_cinematics; i < MAX_CINEMATICS; i++, cin++){
		if (!cin->playing)
			continue;

		count++;
		bytes += cin->frameWidth * cin->frameHeight * 8;

		Com_Printf("%4i: ", i);

		Com_Printf("%4i %4i ", cin->frameWidth, cin->frameHeight);

		Com_Printf("%5ik ", SIZE_KB(cin->frameWidth * cin->frameHeight * 8));

		Com_Printf("%3i ", cin->frameRate);

		Com_Printf("%s\n", cin->name);
	}

	Com_Printf("-------------------------------------------\n");
	Com_Printf("%i total cinematics\n", count);
	Com_Printf("%.2f MB of cinematic data\n", SIZE_MB_FLOAT(bytes));
	Com_Printf("\n");
}


/*
 ==============================================================================

 INITIALIZATION & SHUTDOWN

 ==============================================================================
*/


/*
 ==================
 CIN_Init
 ==================
*/
void CIN_Init (){

	float	f;
	short	s;
	int		i;

	// Add commands
	Cmd_AddCommand("playCinematic", CIN_PlayCinematic_f, "Plays a cinematic", Cmd_ArgCompletion_VideoName);
	Cmd_AddCommand("listCinematics", CIN_ListCinematics_f, "Lists playing cinematics", NULL);

	// Build YUV-to-RGB tables
	for (i = 0; i < 256; i++){
		f = (float)(i - 128);

		cin_v2rTable[i] = (short)(f *  1.40200f);
		cin_u2gTable[i] = (short)(f * -0.34414f);
		cin_v2gTable[i] = (short)(f * -0.71414f);
		cin_u2bTable[i] = (short)(f *  1.77200f);
	}

	// Build square table
	for (i = 0; i < 128; i++){
		s = (short)Square(i);

		cin_sqrTable[i] = s;
		cin_sqrTable[i+128] = -s;
	}
}

/*
 ==================
 CIN_Shutdown
 ==================
*/
void CIN_Shutdown (){

	cinematic_t	*cin;
	int			i;

	// Remove commands
	Cmd_RemoveCommand("playCinematic");
	Cmd_RemoveCommand("listCinematics");

	// Stop all the cinematics
	cls.playingCinematic = false;

	for (i = 0, cin = cin_cinematics; i < MAX_CINEMATICS; i++, cin++){
		if (!cin->playing)
			continue;

		// Free the frame buffers
		if (cin->frameBuffer[0])
			Mem_Free(cin->frameBuffer[0]);
		if (cin->frameBuffer[1])
			Mem_Free(cin->frameBuffer[1]);

		// Close the file
		if (cin->file)
			FS_CloseFile(cin->file);
	}

	// Clear cinematic list
	Mem_Fill(cin_cinematics, 0, sizeof(cin_cinematics));
}